home *** CD-ROM | disk | FTP | other *** search
/ Clickx 115 / Clickx 115.iso / software / tools / windows / tails-i386-0.16.iso / live / filesystem.squashfs / usr / share / arm / util / uiTools.py < prev   
Encoding:
Python Source  |  2012-05-18  |  25.1 KB  |  747 lines

  1. """
  2. Toolkit for common ui tasks when working with curses. This provides a quick and
  3. easy method of providing the following interface components:
  4. - preinitialized curses color attributes
  5. - unit conversion for labels
  6. """
  7.  
  8. import os
  9. import sys
  10. import curses
  11.  
  12. from curses.ascii import isprint
  13. from util import enum, log
  14.  
  15. # colors curses can handle
  16. COLOR_LIST = {"red": curses.COLOR_RED,        "green": curses.COLOR_GREEN,
  17.               "yellow": curses.COLOR_YELLOW,  "blue": curses.COLOR_BLUE,
  18.               "cyan": curses.COLOR_CYAN,      "magenta": curses.COLOR_MAGENTA,
  19.               "black": curses.COLOR_BLACK,    "white": curses.COLOR_WHITE}
  20.  
  21. # boolean for if we have color support enabled, None not yet determined
  22. COLOR_IS_SUPPORTED = None
  23.  
  24. # mappings for getColor() - this uses the default terminal color scheme if
  25. # color support is unavailable
  26. COLOR_ATTR_INITIALIZED = False
  27. COLOR_ATTR = dict([(color, 0) for color in COLOR_LIST])
  28.  
  29. # value tuples for label conversions (bits / bytes / seconds, short label, long label)
  30. SIZE_UNITS_BITS =  [(140737488355328.0, " Pb", " Petabit"), (137438953472.0, " Tb", " Terabit"),
  31.                     (134217728.0, " Gb", " Gigabit"),       (131072.0, " Mb", " Megabit"),
  32.                     (128.0, " Kb", " Kilobit"),             (0.125, " b", " Bit")]
  33. SIZE_UNITS_BYTES = [(1125899906842624.0, " PB", " Petabyte"), (1099511627776.0, " TB", " Terabyte"),
  34.                     (1073741824.0, " GB", " Gigabyte"),       (1048576.0, " MB", " Megabyte"),
  35.                     (1024.0, " KB", " Kilobyte"),             (1.0, " B", " Byte")]
  36. TIME_UNITS = [(86400.0, "d", " day"), (3600.0, "h", " hour"),
  37.               (60.0, "m", " minute"), (1.0, "s", " second")]
  38.  
  39. Ending = enum.Enum("ELLIPSE", "HYPHEN")
  40. SCROLL_KEYS = (curses.KEY_UP, curses.KEY_DOWN, curses.KEY_PPAGE, curses.KEY_NPAGE, curses.KEY_HOME, curses.KEY_END)
  41. CONFIG = {"features.colorInterface": True,
  42.           "features.acsSupport": True,
  43.           "features.printUnicode": True,
  44.           "log.cursesColorSupport": log.INFO,
  45.           "log.configEntryTypeError": log.NOTICE}
  46.  
  47. # Flag indicating if unicode is supported by curses. If None then this has yet
  48. # to be determined.
  49. IS_UNICODE_SUPPORTED = None
  50.  
  51. def loadConfig(config):
  52.   config.update(CONFIG)
  53.   
  54.   CONFIG["features.colorOverride"] = "none"
  55.   colorOverride = config.get("features.colorOverride", "none")
  56.   
  57.   if colorOverride != "none":
  58.     try: setColorOverride(colorOverride)
  59.     except ValueError, exc:
  60.       log.log(CONFIG["log.configEntryTypeError"], exc)
  61.  
  62. def demoGlyphs():
  63.   """
  64.   Displays all ACS options with their corresponding representation. These are
  65.   undocumented in the pydocs. For more information see the following man page:
  66.   http://www.mkssoftware.com/docs/man5/terminfo.5.asp
  67.   """
  68.   
  69.   try: curses.wrapper(_showGlyphs)
  70.   except KeyboardInterrupt: pass # quit
  71.  
  72. def _showGlyphs(stdscr):
  73.   """
  74.   Renders a chart with the ACS glyphs.
  75.   """
  76.   
  77.   # allows things like semi-transparent backgrounds
  78.   try: curses.use_default_colors()
  79.   except curses.error: pass
  80.   
  81.   # attempts to make the cursor invisible
  82.   try: curses.curs_set(0)
  83.   except curses.error: pass
  84.   
  85.   acsOptions = [item for item in curses.__dict__.items() if item[0].startswith("ACS_")]
  86.   acsOptions.sort(key=lambda i: (i[1])) # order by character codes
  87.   
  88.   # displays a chart with all the glyphs and their representations
  89.   height, width = stdscr.getmaxyx()
  90.   if width < 30: return # not enough room to show a column
  91.   columns = width / 30
  92.   
  93.   # display title
  94.   stdscr.addstr(0, 0, "Curses Glyphs:", curses.A_STANDOUT)
  95.   
  96.   x, y = 0, 1
  97.   while acsOptions:
  98.     name, keycode = acsOptions.pop(0)
  99.     stdscr.addstr(y, x * 30, "%s (%i)" % (name, keycode))
  100.     stdscr.addch(y, (x * 30) + 25, keycode)
  101.     
  102.     x += 1
  103.     if x >= columns:
  104.       x, y = 0, y + 1
  105.       if y >= height: break
  106.   
  107.   stdscr.getch() # quit on keyboard input
  108.  
  109. def isUnicodeAvailable():
  110.   """
  111.   True if curses has wide character support, false otherwise or if it can't be
  112.   determined.
  113.   """
  114.   
  115.   global IS_UNICODE_SUPPORTED
  116.   if IS_UNICODE_SUPPORTED == None:
  117.     import sysTools
  118.     
  119.     if CONFIG["features.printUnicode"]:
  120.       # Checks if our LANG variable is unicode. This is what will be respected
  121.       # when printing multi-byte characters after calling...
  122.       # locale.setlocale(locale.LC_ALL, '')
  123.       # 
  124.       # so if the LANG isn't unicode then setting this would be pointless.
  125.       
  126.       isLangUnicode = "utf-" in os.environ.get("LANG", "").lower()
  127.       IS_UNICODE_SUPPORTED = isLangUnicode and _isWideCharactersAvailable()
  128.     else: IS_UNICODE_SUPPORTED = False
  129.   
  130.   return IS_UNICODE_SUPPORTED
  131.  
  132. def getPrintable(line, keepNewlines = True):
  133.   """
  134.   Provides the line back with non-printable characters stripped.
  135.   
  136.   Arguments:
  137.     line          - string to be processed
  138.     stripNewlines - retains newlines if true, stripped otherwise
  139.   """
  140.   
  141.   line = line.replace('\xc2', "'")
  142.   line = "".join([char for char in line if (isprint(char) or (keepNewlines and char == "\n"))])
  143.   return line
  144.  
  145. def isColorSupported():
  146.   """
  147.   True if the display supports showing color, false otherwise.
  148.   """
  149.   
  150.   if COLOR_IS_SUPPORTED == None: _initColors()
  151.   return COLOR_IS_SUPPORTED
  152.  
  153. def getColor(color):
  154.   """
  155.   Provides attribute corresponding to a given text color. Supported colors
  156.   include:
  157.   red       green     yellow    blue
  158.   cyan      magenta   black     white
  159.   
  160.   If color support isn't available or colors can't be initialized then this uses the 
  161.   terminal's default coloring scheme.
  162.   
  163.   Arguments:
  164.     color - name of the foreground color to be returned
  165.   """
  166.   
  167.   colorOverride = getColorOverride()
  168.   if colorOverride: color = colorOverride
  169.   if not COLOR_ATTR_INITIALIZED: _initColors()
  170.   return COLOR_ATTR[color]
  171.  
  172. def setColorOverride(color = None):
  173.   """
  174.   Overwrites all requests for color with the given color instead. This raises
  175.   a ValueError if the color is invalid.
  176.   
  177.   Arguments:
  178.     color - name of the color to overwrite requests with, None to use normal
  179.             coloring
  180.   """
  181.   
  182.   if color == None:
  183.     CONFIG["features.colorOverride"] = "none"
  184.   elif color in COLOR_LIST.keys():
  185.     CONFIG["features.colorOverride"] = color
  186.   else: raise ValueError("\"%s\" isn't a valid color" % color)
  187.  
  188. def getColorOverride():
  189.   """
  190.   Provides the override color used by the interface, None if it isn't set.
  191.   """
  192.   
  193.   colorOverride = CONFIG.get("features.colorOverride", "none")
  194.   if colorOverride == "none": return None
  195.   else: return colorOverride
  196.  
  197. def cropStr(msg, size, minWordLen = 4, minCrop = 0, endType = Ending.ELLIPSE, getRemainder = False):
  198.   """
  199.   Provides the msg constrained to the given length, truncating on word breaks.
  200.   If the last words is long this truncates mid-word with an ellipse. If there
  201.   isn't room for even a truncated single word (or one word plus the ellipse if
  202.   including those) then this provides an empty string. If a cropped string ends
  203.   with a comma or period then it's stripped (unless we're providing the
  204.   remainder back). Examples:
  205.   
  206.   cropStr("This is a looooong message", 17)
  207.   "This is a looo..."
  208.   
  209.   cropStr("This is a looooong message", 12)
  210.   "This is a..."
  211.   
  212.   cropStr("This is a looooong message", 3)
  213.   ""
  214.   
  215.   Arguments:
  216.     msg          - source text
  217.     size         - room available for text
  218.     minWordLen   - minimum characters before which a word is dropped, requires
  219.                    whole word if None
  220.     minCrop      - minimum characters that must be dropped if a word's cropped
  221.     endType      - type of ending used when truncating:
  222.                    None - blank ending
  223.                    Ending.ELLIPSE - includes an ellipse
  224.                    Ending.HYPHEN - adds hyphen when breaking words
  225.     getRemainder - returns a tuple instead, with the second part being the
  226.                    cropped portion of the message
  227.   """
  228.   
  229.   # checks if there's room for the whole message
  230.   if len(msg) <= size:
  231.     if getRemainder: return (msg, "")
  232.     else: return msg
  233.   
  234.   # avoids negative input
  235.   size = max(0, size)
  236.   if minWordLen != None: minWordLen = max(0, minWordLen)
  237.   minCrop = max(0, minCrop)
  238.   
  239.   # since we're cropping, the effective space available is less with an
  240.   # ellipse, and cropping words requires an extra space for hyphens
  241.   if endType == Ending.ELLIPSE: size -= 3
  242.   elif endType == Ending.HYPHEN and minWordLen != None: minWordLen += 1
  243.   
  244.   # checks if there isn't the minimum space needed to include anything
  245.   lastWordbreak = msg.rfind(" ", 0, size + 1)
  246.   
  247.   if lastWordbreak == -1:
  248.     # we're splitting the first word
  249.     if minWordLen == None or size < minWordLen:
  250.       if getRemainder: return ("", msg)
  251.       else: return ""
  252.     
  253.     includeCrop = True
  254.   else:
  255.     lastWordbreak = len(msg[:lastWordbreak].rstrip()) # drops extra ending whitespaces
  256.     if (minWordLen != None and size < minWordLen) or (minWordLen == None and lastWordbreak < 1):
  257.       if getRemainder: return ("", msg)
  258.       else: return ""
  259.     
  260.     if minWordLen == None: minWordLen = sys.maxint
  261.     includeCrop = size - lastWordbreak - 1 >= minWordLen
  262.   
  263.   # if there's a max crop size then make sure we're cropping at least that many characters
  264.   if includeCrop and minCrop:
  265.     nextWordbreak = msg.find(" ", size)
  266.     if nextWordbreak == -1: nextWordbreak = len(msg)
  267.     includeCrop = nextWordbreak - size + 1 >= minCrop
  268.   
  269.   if includeCrop:
  270.     returnMsg, remainder = msg[:size], msg[size:]
  271.     if endType == Ending.HYPHEN:
  272.       remainder = returnMsg[-1] + remainder
  273.       returnMsg = returnMsg[:-1].rstrip() + "-"
  274.   else: returnMsg, remainder = msg[:lastWordbreak], msg[lastWordbreak:]
  275.   
  276.   # if this is ending with a comma or period then strip it off
  277.   if not getRemainder and returnMsg and returnMsg[-1] in (",", "."):
  278.     returnMsg = returnMsg[:-1]
  279.   
  280.   if endType == Ending.ELLIPSE:
  281.     returnMsg = returnMsg.rstrip() + "..."
  282.   
  283.   if getRemainder: return (returnMsg, remainder)
  284.   else: return returnMsg
  285.  
  286. def padStr(msg, size, cropExtra = False):
  287.   """
  288.   Provides the string padded with whitespace to the given length.
  289.   
  290.   Arguments:
  291.     msg       - string to be padded
  292.     size      - length to be padded to
  293.     cropExtra - crops string if it's longer than the size if true
  294.   """
  295.   
  296.   if cropExtra: msg = msg[:size]
  297.   return ("%%-%is" % size) % msg
  298.  
  299. def camelCase(label, divider = "_", joiner = " "):
  300.   """
  301.   Converts the given string to camel case, ie:
  302.   >>> camelCase("I_LIKE_PEPPERJACK!")
  303.   'I Like Pepperjack!'
  304.   
  305.   Arguments:
  306.     label   - input string to be converted
  307.     divider - character to be used for word breaks
  308.     joiner  - character used to fill between word breaks
  309.   """
  310.   
  311.   words = []
  312.   for entry in label.split(divider):
  313.     if len(entry) == 0: words.append("")
  314.     elif len(entry) == 1: words.append(entry.upper())
  315.     else: words.append(entry[0].upper() + entry[1:].lower())
  316.   
  317.   return joiner.join(words)
  318.  
  319. def drawBox(panel, top, left, width, height, attr=curses.A_NORMAL):
  320.   """
  321.   Draws a box in the panel with the given bounds.
  322.   
  323.   Arguments:
  324.     panel  - panel in which to draw
  325.     top    - vertical position of the box's top
  326.     left   - horizontal position of the box's left side
  327.     width  - width of the drawn box
  328.     height - height of the drawn box
  329.     attr   - text attributes
  330.   """
  331.   
  332.   # draws the top and bottom
  333.   panel.hline(top, left + 1, width - 2, attr)
  334.   panel.hline(top + height - 1, left + 1, width - 2, attr)
  335.   
  336.   # draws the left and right sides
  337.   panel.vline(top + 1, left, height - 2, attr)
  338.   panel.vline(top + 1, left + width - 1, height - 2, attr)
  339.   
  340.   # draws the corners
  341.   panel.addch(top, left, curses.ACS_ULCORNER, attr)
  342.   panel.addch(top, left + width - 1, curses.ACS_URCORNER, attr)
  343.   panel.addch(top + height - 1, left, curses.ACS_LLCORNER, attr)
  344.  
  345. def isSelectionKey(key):
  346.   """
  347.   Returns true if the keycode matches the enter or space keys.
  348.   
  349.   Argument:
  350.     key - keycode to be checked
  351.   """
  352.   
  353.   return key in (curses.KEY_ENTER, 10, ord(' '))
  354.  
  355. def isScrollKey(key):
  356.   """
  357.   Returns true if the keycode is recognized by the getScrollPosition function
  358.   for scrolling.
  359.   
  360.   Argument:
  361.     key - keycode to be checked
  362.   """
  363.   
  364.   return key in SCROLL_KEYS
  365.  
  366. def getScrollPosition(key, position, pageHeight, contentHeight, isCursor = False):
  367.   """
  368.   Parses navigation keys, providing the new scroll possition the panel should
  369.   use. Position is always between zero and (contentHeight - pageHeight). This
  370.   handles the following keys:
  371.   Up / Down - scrolls a position up or down
  372.   Page Up / Page Down - scrolls by the pageHeight
  373.   Home - top of the content
  374.   End - bottom of the content
  375.   
  376.   This provides the input position if the key doesn't correspond to the above.
  377.   
  378.   Arguments:
  379.     key           - keycode for the user's input
  380.     position      - starting position
  381.     pageHeight    - size of a single screen's worth of content
  382.     contentHeight - total lines of content that can be scrolled
  383.     isCursor      - tracks a cursor position rather than scroll if true
  384.   """
  385.   
  386.   if isScrollKey(key):
  387.     shift = 0
  388.     if key == curses.KEY_UP: shift = -1
  389.     elif key == curses.KEY_DOWN: shift = 1
  390.     elif key == curses.KEY_PPAGE: shift = -pageHeight + 1 if isCursor else -pageHeight
  391.     elif key == curses.KEY_NPAGE: shift = pageHeight - 1 if isCursor else pageHeight
  392.     elif key == curses.KEY_HOME: shift = -contentHeight
  393.     elif key == curses.KEY_END: shift = contentHeight
  394.     
  395.     # returns the shift, restricted to valid bounds
  396.     maxLoc = contentHeight - 1 if isCursor else contentHeight - pageHeight
  397.     return max(0, min(position + shift, maxLoc))
  398.   else: return position
  399.  
  400. def getSizeLabel(bytes, decimal = 0, isLong = False, isBytes=True):
  401.   """
  402.   Converts byte count into label in its most significant units, for instance
  403.   7500 bytes would return "7 KB". If the isLong option is used this expands
  404.   unit labels to be the properly pluralized full word (for instance 'Kilobytes'
  405.   rather than 'KB'). Units go up through PB.
  406.   
  407.   Example Usage:
  408.     getSizeLabel(2000000) = '1 MB'
  409.     getSizeLabel(1050, 2) = '1.02 KB'
  410.     getSizeLabel(1050, 3, True) = '1.025 Kilobytes'
  411.   
  412.   Arguments:
  413.     bytes   - source number of bytes for conversion
  414.     decimal - number of decimal digits to be included
  415.     isLong  - expands units label
  416.     isBytes - provides units in bytes if true, bits otherwise
  417.   """
  418.   
  419.   if isBytes: return _getLabel(SIZE_UNITS_BYTES, bytes, decimal, isLong)
  420.   else: return _getLabel(SIZE_UNITS_BITS, bytes, decimal, isLong)
  421.  
  422. def getTimeLabel(seconds, decimal = 0, isLong = False):
  423.   """
  424.   Converts seconds into a time label truncated to its most significant units,
  425.   for instance 7500 seconds would return "2h". Units go up through days.
  426.   
  427.   This defaults to presenting single character labels, but if the isLong option
  428.   is used this expands labels to be the full word (space included and properly
  429.   pluralized). For instance, "4h" would be "4 hours" and "1m" would become
  430.   "1 minute".
  431.   
  432.   Example Usage:
  433.     getTimeLabel(10000) = '2h'
  434.     getTimeLabel(61, 1, True) = '1.0 minute'
  435.     getTimeLabel(61, 2, True) = '1.01 minutes'
  436.   
  437.   Arguments:
  438.     seconds - source number of seconds for conversion
  439.     decimal - number of decimal digits to be included
  440.     isLong  - expands units label
  441.   """
  442.   
  443.   return _getLabel(TIME_UNITS, seconds, decimal, isLong)
  444.  
  445. def getTimeLabels(seconds, isLong = False):
  446.   """
  447.   Provides a list containing label conversions for each time unit, starting
  448.   with its most significant units on down. Any counts that evaluate to zero are
  449.   omitted.
  450.   
  451.   Example Usage:
  452.     getTimeLabels(400) = ['6m', '40s']
  453.     getTimeLabels(3640, True) = ['1 hour', '40 seconds']
  454.   
  455.   Arguments:
  456.     seconds - source number of seconds for conversion
  457.     isLong  - expands units label
  458.   """
  459.   
  460.   timeLabels = []
  461.   
  462.   for countPerUnit, _, _ in TIME_UNITS:
  463.     if seconds >= countPerUnit:
  464.       timeLabels.append(_getLabel(TIME_UNITS, seconds, 0, isLong))
  465.       seconds %= countPerUnit
  466.   
  467.   return timeLabels
  468.  
  469. def getShortTimeLabel(seconds):
  470.   """
  471.   Provides a time in the following format:
  472.   [[dd-]hh:]mm:ss
  473.   
  474.   Arguments:
  475.     seconds - source number of seconds for conversion
  476.   """
  477.   
  478.   timeComp = {}
  479.   
  480.   for amount, _, label in TIME_UNITS:
  481.     count = int(seconds / amount)
  482.     seconds %= amount
  483.     timeComp[label.strip()] = count
  484.   
  485.   labelPrefix = ""
  486.   if timeComp["day"]:
  487.     labelPrefix = "%i-%02i:" % (timeComp["day"], timeComp["hour"])
  488.   elif timeComp["hour"]:
  489.     labelPrefix = "%02i:" % timeComp["hour"]
  490.   
  491.   return "%s%02i:%02i" % (labelPrefix, timeComp["minute"], timeComp["second"])
  492.  
  493. def parseShortTimeLabel(timeEntry):
  494.   """
  495.   Provides the number of seconds corresponding to the formatting used for the
  496.   cputime and etime fields of ps:
  497.   [[dd-]hh:]mm:ss or mm:ss.ss
  498.   
  499.   If the input entry is malformed then this raises a ValueError.
  500.   
  501.   Arguments:
  502.     timeEntry - formatting ps time entry
  503.   """
  504.   
  505.   days, hours, minutes, seconds = 0, 0, 0, 0
  506.   errorMsg = "invalidly formatted ps time entry: %s" % timeEntry
  507.   
  508.   dateDivider = timeEntry.find("-")
  509.   if dateDivider != -1:
  510.     days = int(timeEntry[:dateDivider])
  511.     timeEntry = timeEntry[dateDivider+1:]
  512.   
  513.   timeComp = timeEntry.split(":")
  514.   if len(timeComp) == 3:
  515.     hours, minutes, seconds = timeComp
  516.   elif len(timeComp) == 2:
  517.     minutes, seconds = timeComp
  518.     seconds = round(float(seconds))
  519.   else:
  520.     raise ValueError(errorMsg)
  521.   
  522.   try:
  523.     timeSum = int(seconds)
  524.     timeSum += int(minutes) * 60
  525.     timeSum += int(hours) * 3600
  526.     timeSum += int(days) * 86400
  527.     return timeSum
  528.   except ValueError:
  529.     raise ValueError(errorMsg)
  530.  
  531. class Scroller:
  532.   """
  533.   Tracks the scrolling position when there might be a visible cursor. This
  534.   expects that there is a single line displayed per an entry in the contents.
  535.   """
  536.   
  537.   def __init__(self, isCursorEnabled):
  538.     self.scrollLoc, self.cursorLoc = 0, 0
  539.     self.cursorSelection = None
  540.     self.isCursorEnabled = isCursorEnabled
  541.   
  542.   def getScrollLoc(self, content, pageHeight):
  543.     """
  544.     Provides the scrolling location, taking into account its cursor's location
  545.     content size, and page height.
  546.     
  547.     Arguments:
  548.       content    - displayed content
  549.       pageHeight - height of the display area for the content
  550.     """
  551.     
  552.     if content and pageHeight:
  553.       self.scrollLoc = max(0, min(self.scrollLoc, len(content) - pageHeight + 1))
  554.       
  555.       if self.isCursorEnabled:
  556.         self.getCursorSelection(content) # resets the cursor location
  557.         
  558.         # makes sure the cursor is visible
  559.         if self.cursorLoc < self.scrollLoc:
  560.           self.scrollLoc = self.cursorLoc
  561.         elif self.cursorLoc > self.scrollLoc + pageHeight - 1:
  562.           self.scrollLoc = self.cursorLoc - pageHeight + 1
  563.       
  564.       # checks if the bottom would run off the content (this could be the
  565.       # case when the content's size is dynamic and entries are removed)
  566.       if len(content) > pageHeight:
  567.         self.scrollLoc = min(self.scrollLoc, len(content) - pageHeight)
  568.     
  569.     return self.scrollLoc
  570.   
  571.   def getCursorSelection(self, content):
  572.     """
  573.     Provides the selected item in the content. This is the same entry until
  574.     the cursor moves or it's no longer available (in which case it moves on to
  575.     the next entry).
  576.     
  577.     Arguments:
  578.       content - displayed content
  579.     """
  580.     
  581.     # TODO: needs to handle duplicate entries when using this for the
  582.     # connection panel
  583.     
  584.     if not self.isCursorEnabled: return None
  585.     elif not content:
  586.       self.cursorLoc, self.cursorSelection = 0, None
  587.       return None
  588.     
  589.     self.cursorLoc = min(self.cursorLoc, len(content) - 1)
  590.     if self.cursorSelection != None and self.cursorSelection in content:
  591.       # moves cursor location to track the selection
  592.       self.cursorLoc = content.index(self.cursorSelection)
  593.     else:
  594.       # select the next closest entry
  595.       self.cursorSelection = content[self.cursorLoc]
  596.     
  597.     return self.cursorSelection
  598.   
  599.   def handleKey(self, key, content, pageHeight):
  600.     """
  601.     Moves either the scroll or cursor according to the given input.
  602.     
  603.     Arguments:
  604.       key        - key code of user input
  605.       content    - displayed content
  606.       pageHeight - height of the display area for the content
  607.     """
  608.     
  609.     if self.isCursorEnabled:
  610.       self.getCursorSelection(content) # resets the cursor location
  611.       startLoc = self.cursorLoc
  612.     else: startLoc = self.scrollLoc
  613.     
  614.     newLoc = getScrollPosition(key, startLoc, pageHeight, len(content), self.isCursorEnabled)
  615.     if startLoc != newLoc:
  616.       if self.isCursorEnabled: self.cursorSelection = content[newLoc]
  617.       else: self.scrollLoc = newLoc
  618.       return True
  619.     else: return False
  620.  
  621. def _getLabel(units, count, decimal, isLong):
  622.   """
  623.   Provides label corresponding to units of the highest significance in the
  624.   provided set. This rounds down (ie, integer truncation after visible units).
  625.   
  626.   Arguments:
  627.     units   - type of units to be used for conversion, a tuple containing
  628.               (countPerUnit, shortLabel, longLabel)
  629.     count   - number of base units being converted
  630.     decimal - decimal precision of label
  631.     isLong  - uses the long label if true, short label otherwise
  632.   """
  633.   
  634.   format = "%%.%if" % decimal
  635.   if count < 1:
  636.     unitsLabel = units[-1][2] + "s" if isLong else units[-1][1]
  637.     return "%s%s" % (format % count, unitsLabel)
  638.   
  639.   for countPerUnit, shortLabel, longLabel in units:
  640.     if count >= countPerUnit:
  641.       if count * 10 ** decimal % countPerUnit * 10 ** decimal == 0:
  642.         # even division, keep it simple
  643.         countLabel = format % (count / countPerUnit)
  644.       else:
  645.         # unfortunately the %f formatting has no method of rounding down, so
  646.         # reducing value to only concern the digits that are visible - note
  647.         # that this doesn't work with minuscule values (starts breaking down at
  648.         # around eight decimal places) or edge cases when working with powers
  649.         # of two
  650.         croppedCount = count - (count % (countPerUnit / (10 ** decimal)))
  651.         countLabel = format % (croppedCount / countPerUnit)
  652.       
  653.       if isLong:
  654.         # plural if any of the visible units make it greater than one (for
  655.         # instance 1.0003 is plural but 1.000 isn't)
  656.         if decimal > 0: isPlural = count >= (countPerUnit + countPerUnit / (10 ** decimal))
  657.         else: isPlural = count >= countPerUnit * 2
  658.         return countLabel + longLabel + ("s" if isPlural else "")
  659.       else: return countLabel + shortLabel
  660.  
  661. def _isWideCharactersAvailable():
  662.   """
  663.   True if curses has wide character support (which is required to print
  664.   unicode). False otherwise.
  665.   """
  666.   
  667.   try:
  668.     # gets the dynamic library used by the interpretor for curses
  669.     
  670.     import _curses
  671.     cursesLib = _curses.__file__
  672.     
  673.     # Uses 'ldd' (Linux) or 'otool -L' (Mac) to determine the curses
  674.     # library dependencies.
  675.     # 
  676.     # atagar@fenrir:~/Desktop$ ldd /usr/lib/python2.6/lib-dynload/_curses.so
  677.     #   linux-gate.so.1 =>  (0x00a51000)
  678.     #   libncursesw.so.5 => /lib/libncursesw.so.5 (0x00faa000)
  679.     #   libpthread.so.0 => /lib/tls/i686/cmov/libpthread.so.0 (0x002f1000)
  680.     #   libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00158000)
  681.     #   libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0x00398000)
  682.     #   /lib/ld-linux.so.2 (0x00ca8000)
  683.     # 
  684.     # atagar$ otool -L /System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/lib-dynload/_curses.so
  685.     # /System/Library/Frameworks/Python.framework/Versions/2.5/lib/python2.5/lib-dynload/_curses.so:
  686.     #   /usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0)
  687.     #   /usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)
  688.     #   /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 111.1.6)
  689.     
  690.     libDependencyLines = None
  691.     if sysTools.isAvailable("ldd"):
  692.       libDependencyLines = sysTools.call("ldd %s" % cursesLib)
  693.     elif sysTools.isAvailable("otool"):
  694.       libDependencyLines = sysTools.call("otool -L %s" % cursesLib)
  695.     
  696.     if libDependencyLines:
  697.       for line in libDependencyLines:
  698.         if "libncursesw" in line: return True
  699.   except: pass
  700.   
  701.   return False
  702.  
  703. def _initColors():
  704.   """
  705.   Initializes color mappings usable by curses. This can only be done after
  706.   calling curses.initscr().
  707.   """
  708.   
  709.   global COLOR_ATTR_INITIALIZED, COLOR_IS_SUPPORTED
  710.   if not COLOR_ATTR_INITIALIZED:
  711.     # hack to replace all ACS characters with '+' if ACS support has been
  712.     # manually disabled
  713.     if not CONFIG["features.acsSupport"]:
  714.       for item in curses.__dict__:
  715.         if item.startswith("ACS_"):
  716.           curses.__dict__[item] = ord('+')
  717.       
  718.       # replace a few common border pipes that are better rendered as '|' or
  719.       # '-' instead
  720.       
  721.       curses.ACS_SBSB = ord('|')
  722.       curses.ACS_VLINE = ord('|')
  723.       curses.ACS_BSBS = ord('-')
  724.       curses.ACS_HLINE = ord('-')
  725.     
  726.     COLOR_ATTR_INITIALIZED = True
  727.     COLOR_IS_SUPPORTED = False
  728.     if not CONFIG["features.colorInterface"]: return
  729.     
  730.     try: COLOR_IS_SUPPORTED = curses.has_colors()
  731.     except curses.error: return # initscr hasn't been called yet
  732.     
  733.     # initializes color mappings if color support is available
  734.     if COLOR_IS_SUPPORTED:
  735.       colorpair = 0
  736.       log.log(CONFIG["log.cursesColorSupport"], "Terminal color support detected and enabled")
  737.       
  738.       for colorName in COLOR_LIST:
  739.         fgColor = COLOR_LIST[colorName]
  740.         bgColor = -1 # allows for default (possibly transparent) background
  741.         colorpair += 1
  742.         curses.init_pair(colorpair, fgColor, bgColor)
  743.         COLOR_ATTR[colorName] = curses.color_pair(colorpair)
  744.     else:
  745.       log.log(CONFIG["log.cursesColorSupport"], "Terminal color support unavailable")
  746.  
  747.